#!/usr/bin/env node

'use strict';

const assert = require('bsert');
const fs = require('bfile');
const Path = require('path');
const bio = require('bufio');
const blake2b = require('bcrypto/lib/blake2b');
const sha256 = require('bcrypto/lib/sha256');
const merkle = require('bcrypto/lib/mrkl');
const AirdropKey = require('../lib/key');
const tree = require('../etc/tree.json');
const {reward: TREE_REWARD} = tree;

/*
 * Constants
 */

assert(process.argv.length > 2);

const PREFIX = process.argv[2];
const FAUCET_FILE = Path.resolve(__dirname, '..', 'build', 'faucet.bin');
const FAUCET_JSON = Path.resolve(__dirname, '..', 'etc', 'faucet.json');
const PROOF_FILE = Path.resolve(__dirname, '..', 'build', 'proof.json');

/*
 * Main
 */

async function main() {
  const hasher = new Hasher();
  const [checksum, proofChecksum] = await hasher.hash();
  const leaves = hasher.leaves;

  console.log('Wrote merkle tree with %d leaves.', leaves.length);

  const root = hasher.hashTree();

  console.log('Tree Checksum: %s', checksum.toString('hex'));
  console.log('Proof Checksum: %s', proofChecksum.toString('hex'));
  console.log('Tree Root: %s', root.toString('hex'));
  console.log('Leaves: %d', leaves.length);
  console.log('Depth: %d', getDepth(leaves.length));
  console.log('Participants: %d', hasher.participants);
  console.log('Faucet Total: %d', hasher.faucet);
  console.log('Shares: %d', hasher.shares);
  console.log('Sponsors: %d', hasher.sponsors);
  console.log('Creators: %d', hasher.creators);
  console.log('FOSS: %d', hasher.foss);
  console.log('Naming: %d', hasher.naming);
  console.log('External Total: %d', hasher.external);

  const json = JSON.stringify({
    checksum: checksum.toString('hex'),
    root: root.toString('hex'),
    leaves: leaves.length,
    depth: getDepth(leaves.length),
    participants: hasher.participants,
    faucet: hasher.faucet,
    shares: hasher.shares,
    sponsors: hasher.sponsors,
    creators: hasher.creators,
    foss: hasher.foss,
    naming: hasher.naming,
    external: hasher.external,
    proofChecksum: proofChecksum.toString('hex')
  }, null, 2);

  return fs.writeFile(FAUCET_JSON, json + '\n');
}

/*
 * Hasher
 */

class Hasher {
  constructor() {
    this.leaves = [];
    this.proof = [];
    this.seen = new Set();
    this.participants = 0;
    this.faucet = 0;
    this.shares = 0;
    this.sponsors = 0;
    this.creators = 0;
    this.foss = 0;
    this.naming = 0;
    this.external = 0;
  }

  async hash() {
    await this.hashSponsors();
    await this.hashCreators();
    await this.hashFOSS();
    await this.hashNaming();
    await this.hashFaucet();

    return [
      await this.writeTree(),
      await this.writeProof()
    ];
  }

  check(hash) {
    // Avoid duplicate hashes.
    assert(Buffer.isBuffer(hash));

    const hex = hash.toString('hex');

    if (this.seen.has(hex))
      throw new Error(`Already seen key: ${hex}.`);

    this.seen.add(hex);
  }

  async hashSponsors() {
    const sponsors = await readJSON(PREFIX, 'sponsors.json');

    for (const {address, value} of sponsors) {
      const key = AirdropKey.fromAddress(address, value, true);
      const hash = key.hash();

      this.check(hash);
      this.leaves.push(hash);
      this.proof.push([hash, address, value, true]);
      this.sponsors += 1;
      this.external += value;
    }

    console.log('Valid sponsor addresses: %d', sponsors.length);
  }

  async hashCreators() {
    const creators = await readJSON(PREFIX, 'creators.json');

    for (const {address, value} of creators) {
      const key = AirdropKey.fromAddress(address, value, false);
      const hash = key.hash();

      this.check(hash);
      this.leaves.push(hash);
      this.proof.push([hash, address, value, false]);
      this.creators += 1;
      this.external += value;
    }

    console.log('Valid creator addresses: %d', creators.length);
  }

  async hashFOSS() {
    const foss = await readJSON(PREFIX, 'foss.json');

    for (const {address, value} of foss) {
      const key = AirdropKey.fromAddress(address, value, true);
      const hash = key.hash();

      this.check(hash);
      this.leaves.push(hash);
      this.proof.push([hash, address, value, true]);
      this.foss += 1;
      this.external += value;
    }

    console.log('Valid FOSS addresses: %d', foss.length);
  }

  async hashNaming() {
    const naming = await readJSON(PREFIX, 'naming.json');

    for (const {address, value} of naming) {
      const key = AirdropKey.fromAddress(address, value, true);
      const hash = key.hash();

      this.check(hash);
      this.leaves.push(hash);
      this.proof.push([hash, address, value, true]);
      this.naming += 1;
      this.external += value;
    }

    console.log('Valid naming addresses: %d', naming.length);
  }

  async hashFaucet() {
    const faucet = await readJSON(PREFIX, 'faucet.json');

    for (const {address, shares} of faucet) {
      const value = TREE_REWARD * shares;
      const key = AirdropKey.fromAddress(address, value, false);
      const hash = key.hash();

      this.check(hash);
      this.leaves.push(hash);
      this.proof.push([hash, address, value, false]);
      this.participants += 1;
      this.faucet += value;
      this.shares += shares;
    }

    console.log('Valid participant addresses: %d', faucet.length);
  }

  async writeProof() {
    const json = this.proof.map(x => x.slice(1));
    const str = JSON.stringify(json, null, 2);
    const data = Buffer.from(str + '\n', 'utf8');

    await fs.writeFile(PROOF_FILE, data);

    return sha256.digest(data);
  }

  async writeTree() {
    const size = 4 + this.leaves.length * 32;
    const bw = bio.write(size);

    this.leaves.sort((x, y) => x.compare(y));

    bw.writeU32(this.leaves.length);

    for (const hash of this.leaves)
      bw.writeBytes(hash);

    const raw = bw.render();

    await fs.writeFile(FAUCET_FILE, raw);

    return sha256.digest(raw);
  }

  hashTree() {
    return merkle.createRoot(blake2b, this.leaves);
  }
}

/*
 * Helpers
 */

async function readText(...args) {
  const file = Path.resolve(...args);
  return fs.readFile(file, 'utf8');
}

async function readJSON(...args) {
  const str = await readText(...args);
  return JSON.parse(str);
}

function getDepth(size) {
  assert((size >>> 0) === size);

  let depth = 0;

  while (size > 1) {
    depth += 1;
    size = (size + 1) >>> 1;
  }

  return depth;
}

/*
 * Execute
 */

main().catch((err) => {
  console.error(err.stack);
  process.exit(1);
});
